카드 컴포넌트 개발#47
Conversation
Summary of ChangesHello @yooolleee, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 다양한 카드 기반 UI 컴포넌트들을 개발하여 사용자 인터페이스의 재사용성과 일관성을 높이는 데 중점을 둡니다. 게시글, 이미지 업로드, 할 일 목록 및 상세 정보 등 여러 기능에 필요한 카드 컴포넌트들을 모듈화하여 구현함으로써, 향후 개발 효율성을 증대시키고 유지보수를 용이하게 합니다. 또한, 개발 환경 설정을 업데이트하여 코드 품질 관리를 강화했습니다. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces several new UI components: ArticleCard, ImageUpload, TaskCard, TaskDetailCard, and KebabMenu, complete with their respective CSS modules and Storybook stories. The ArticleCard displays article details with responsive styling, including logic for formatting dates and like counts. The ImageUpload component allows users to upload multiple images, showing previews and providing client-side validation for file size and type. The TaskCard is a simple card for displaying task labels and counts, while the TaskDetailCard offers a comprehensive view of a task, including assignee, dates, description, completion status, comments, and actions via an integrated KebabMenu. The KebabMenu provides a reusable dropdown for edit and delete functionalities. Additionally, the .vscode/settings.json file was updated to ignore vendor prefixes in CSS linting. Review comments suggest improving accessibility by using next/image/Image with meaningful alt text for article images and uploaded image previews, enhancing user experience by replacing alert() with consistent UI for validation errors in ImageUpload, and improving code maintainability by extracting pure utility functions (formatDate, formatLikeCount) and API interfaces (Writer, Comment) out of components into shared utility and type files. Other suggestions include defining hardcoded values as constants and removing unused CSS classes and component props.
| <div className={styles.rightSection}> | ||
| {image && ( | ||
| <div className={styles.imageWrapper}> | ||
| <img src={image} alt="" className={styles.image} /> |
There was a problem hiding this comment.
게시글 이미지에 일반 <img> 태그 대신 next/image의 Image 컴포넌트를 사용하면 이미지 최적화, 지연 로딩 등의 이점을 얻을 수 있습니다. 또한 alt 속성에는 빈 문자열 대신 게시글 제목 등 의미 있는 대체 텍스트를 제공하여 웹 접근성을 향상시키는 것이 좋습니다. fill 속성을 사용하려면 부모 요소인 .imageWrapper에 position: relative; 스타일을 추가해야 합니다.
| <img src={image} alt="" className={styles.image} /> | |
| <Image src={image} alt={title} fill sizes="(max-width: 767px) 80px, (max-width: 1199px) 100px, 120px" style={{ objectFit: 'cover' }} /> |
There was a problem hiding this comment.
- 왜 300바이트 SVG에 next/image를 쓰는 게 "오히려 손해"일 수 있나?
사용자님 생각이 맞는 이유입니다.
컴포넌트 오버헤드: next/image는 내부적으로 꽤 복잡한 리액트 컴포넌트입니다. 단순 태그나 인라인 는 브라우저가 즉시 그리지만, next/image는 자바스크립트가 실행되고 하이드레이션(Hydration)되는 과정을 거칩니다. 배보다 배꼽이 더 클 수 있죠.
Lazy Loading의 역설: 아주 작은 아이콘은 페이지 로드 시 한꺼번에 가져오는 게 차라리 낫습니다. 굳이 스크롤을 내릴 때마다 하나씩 "나 이제 로드할게!"라고 브라우저에 신호를 보내는 과정 자체가 리소스를 더 먹을 수도 있습니다.
- 그럼에도 AI 코드리뷰가 굳이 권고하는 "궁색한 이유"
AI나 린터(Linter)는 예외 케이스를 고려하지 않고 **'규칙'**만 보기 때문입니다.
규칙의 단순화: "모든 이미지는 next/image를 통한다"라는 규칙을 정해두면, 나중에 누군가 실수로 10MB짜리 SVG(복잡한 지도나 도면 등)를 넣었을 때 사고를 방지할 수 있습니다. 즉, '바보 방지(Fool-proof)' 설계인 셈입니다.
보안 문제 (XSS): SVG 파일 안에 악성 스크립트가 숨겨져 있을 수 있는데, next/image를 통해 처리하면 브라우저가 이를 좀 더 안전하게 다루도록 유도하는 측면이 있습니다. (하지만 이것도 인라인 SVG가 아니면 큰 차이는 없습니다.)
| // 10MB 제한 | ||
| if (file.size > 10 * 1024 * 1024) { | ||
| alert('파일 크기는 10MB를 초과할 수 없습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| // 이미지 파일만 | ||
| if (!file.type.startsWith('image/')) { | ||
| alert('이미지 파일만 업로드할 수 있습니다.'); | ||
| return; | ||
| } |
| const formatDate = (dateString: string): string => { | ||
| const date = new Date(dateString); | ||
| const year = date.getFullYear(); | ||
| const month = (date.getMonth() + 1).toString().padStart(2, '0'); | ||
| const day = date.getDate().toString().padStart(2, '0'); | ||
| return `${year}. ${month}. ${day}`; | ||
| }; | ||
|
|
||
| const formatLikeCount = (count: number): string => { | ||
| if (count >= 1000) { | ||
| return '999+'; | ||
| } | ||
| return count.toString(); | ||
| }; |
|
|
||
| {content && ( | ||
| <p className={styles.preview}> | ||
| {content.length > 100 ? `${content.slice(0, 100)}...` : content} |
| {/* 업로드된 이미지들 */} | ||
| {images.map((imageUrl, index) => ( | ||
| <div key={index} className={`${styles.slot} ${styles[size]}`}> | ||
| <img src={imageUrl} alt="" className={styles.preview} /> |
| .emptySlot { | ||
| opacity: 0.3; | ||
| cursor: not-allowed; | ||
| } |
| interface Writer { | ||
| id: number; | ||
| nickname: string; | ||
| image: string | null; | ||
| } | ||
|
|
||
| /* API 응답 구조 - 댓글 정보 */ | ||
| interface Comment { | ||
| id: number; | ||
| content: string; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| taskId: number; | ||
| userId: number; | ||
| user: { | ||
| id: number; | ||
| nickname: string; | ||
| image: string | null; | ||
| }; | ||
| } |
| } | ||
|
|
||
| interface TaskDetailCardProps { | ||
| id: number; |
| function formatDate(dateString: string): string { | ||
| const date = new Date(dateString); | ||
| const year = date.getFullYear(); | ||
| const month = date.getMonth() + 1; | ||
| const day = date.getDate(); | ||
| const hours = date.getHours(); | ||
| const minutes = date.getMinutes(); | ||
| const period = hours >= 12 ? '오후' : '오전'; | ||
| const displayHours = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours; | ||
|
|
||
| return `${year}년 ${month}월 ${day}일 ${period} ${displayHours}:${minutes.toString().padStart(2, '0')}`; | ||
| } | ||
|
|
||
| function formatCommentDate(dateString: string): string { | ||
| const date = new Date(dateString); | ||
| const year = date.getFullYear(); | ||
| const month = (date.getMonth() + 1).toString().padStart(2, '0'); | ||
| const day = date.getDate().toString().padStart(2, '0'); | ||
|
|
||
| return `${year}. ${month}. ${day}`; | ||
| } |
Summary
이미지 업로드 컴포넌트도 figma 시안상 Card 영역에 있어서 card 폴더안에 넣어두었습니다.
Issue
Scope
포함
특이사항